JWT authentication bypass via jwk header injection
jwk header injection: el servidor usa la clave de verificación embebida en el propio token (parámetro jwk) sin validar su origen, permitiendo firmar tokens con una clave RSA propia.
Target Overview & Vulnerability Summary
El servidor soporta el parámetro jwk en el header del JWT. Este mecanismo se usa a veces para incrustar la clave de verificación directamente en el token, pero la implementación falla al no comprobar si la clave incrustada proviene de una fuente de confianza. El atacante genera su propia pareja RSA, la embebe como JWK en el header y firma el token con su clave privada; el servidor verifica con la clave pública incrustada (controlada por el atacante) y acepta el token.
| Campo | Valor |
|---|---|
| Severidad | Alta (bypass de autenticación → GET /admin y eliminación de carlos) |
| Endpoint vulnerable | POST /login, GET /admin |
| Root cause | El verificador acepta un jwk embebido en el header sin contrastarlo contra ninguna fuente de claves confiable |
Technical Breakdown & Exploitation Flow
El JWT emitido tras autenticarse con wiener:peter usa RS256 (firma con clave pública RSA) y declara un kid:
Headers = {
"kid": "e42a6b7f-d1fc-4434-9852-c21ce5ecead9",
"alg": "RS256"
}
Payload = {
"iss": "portswigger",
"exp": 1757201592,
"sub": "wiener"
}
Signature = "cEA-ET0xb7p2WosCn8u8zbC3Pt3LLTvvetPtXpUlca3C3ms9lih76ODPIzuaAFQLXjZYe2pnhtfPrXh6jFz9ia8ZmOssY2KaEDzjKH4ncfkE6qf8P2wKux3ADcAnMG1Kmy9Hyf-JVOlwB5QmxbYUukHRF-YT2HVciXzcb-HlI7Qijj54WPFV3kaeOBfSWOg-fmC3W76uyI-M_eleLnw9mj5_ju7X7nyORdp7oJmieWV9qBhQ8WJaJmXyaVvi8zXkofVHTI-RoyqLpjmPldiKeHihUJU60VifqO3ZnpZL-wYiXiGYPyV-zeEq4qsXmUmaPGXQdSfsuR9HWOCbBiZQCQ"
El campo kid (Key ID) es un parámetro opcional del header JWT (RFC 7515) que actúa como hint para que el receptor identifique qué clave criptográfica firmó el token. Su formato no está estrictamente definido: puede ser un UUID, un nombre de clave o una referencia a un keystore.
Explotación con JWT Editor (attaque "Embedded JWK")
- En JWT Editor, crear una clave New RSA Key propia (pareja generada por el atacante).
- En Repeater, sobre la request que transporta el JWT, en la pestaña JSON Web Token, modificar el claim
subdewieneraadministrator. - Usar el botón inferior Attack → Embedded JWK y seleccionar la clave RSA creada. Esto inyecta el parámetro
jwk(la clave pública del atacante) dentro del header del JWT y firma el token con la clave privada correspondiente. - Enviar la request y verificar que la ruta de la solicitud GET no apunte al usuario
wiener(usar/my-accounto directamente/admin). - Con
GET /adminautenticado, enviarDELETE /admin?username=carlos.
El JWT resultante se puede intercambiar en el navegador (reemplazando la cookie session) y refrescar para confirmar la sesión de administrador.
Root Cause Analysis
El patrón defectuoso es la confianza ciega en material criptográfico auto-declarado. La especificación JWK permite incrustar una clave en el header, pero un verificador seguro debe usar ese jwk únicamente como hint y contrastarlo contra un almacén de claves confiable. Aquí el servidor toma el jwk del header como la clave de verificación sin validar:
// defectuoso: la clave de verificación proviene del propio token
var header struct {
JWK jwk.Key `json:"jwk"`
Kid string `json:"kid"`
Alg string `json:"alg"`
}
json.Unmarshal(headerBytes, &header)
pub, _ := header.JWK.PublicKey()
token, _ := jwt.Parse(raw, jwt.WithKey(jwa.RS256, pub)) // clave del atacante
Al ser el atacante quien elige el jwk, la verificación siempre es exitosa: se firma con la privada de su propia pareja.
Secure Remediation
- Nunca usar la clave del header como clave de verificación. El verificador debe resolver la clave desde un origen de confianza (el
kidcontra un keystore interno o unjwks_urivalidado por HTTPS):
pub, err := keyStore.Fetch(header.Kid) // solo fuentes autorizadas
if err != nil {
return errors.New("unknown kid")
}
token, err := jwt.Parse(raw, jwt.WithKey(jwa.RS256, pub))
- Ignorar o rechazar el parámetro
jwkdel header cuando la política de verificación es server-side. - Si se admite
jwk(p. ej. en flujos offline), validar que la clave incrustada coincida con una clave previamente registrada por el emisor. - Restringir la lista de
kidaceptados y rechazar claves cuyo origen no se pueda verificar.